/+
Copyright (C) 2007 Myron Alexander (myron.alexander...gmail.com)

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+/

module dbapi.internal.exception2;

import std.stdio;
import std.asserterror;
import std.boxer;
import dbapi.internal.util;

public class Exception2 : Exception {

   this (string msg) {
      super (msg);
   }


   this (string msg, Exception cause) {
      super (msg);
      initCause (cause);
   }


   typeof(this) initCause (Exception cause) {

      if (cast(Exception2)(cause)) {
         //debug {writefln ("Cause is Exception2");}
         m_cause = cast(Exception2)(cause);

      } else {
         //debug {writefln ("Cause is Exception");}
         m_cause = new ExceptionAdaptor (cause);
      }

      m_cause.setParentCause (this);

      return this;
   }


   typeof(this) setLongMsg (string longMsg) {
      m_longMsg = longMsg;
      return this;
   }


   /**
    * Property to return the cause of this exception. If none is set, then null
    * is returned.
    */
   Exception2 cause () {
      return m_cause;
   }


   /**
    * Property to return the long message. If no long message is set, the
    * short message is returned.
    */
   string longMsg () {

      if (m_longMsg is null) {
         return msg;

      } else {

         int[2] range = [0,0];

         //~ debug {
            //~ writefln ("----------------------------------------------------------");

            //~ writefln ("%d %d", range[0], range[1]);
         //~ }

         bool findNextProperty () {

            while (true) {
               // Find the first occurance of the "$(" marker. If found, check that
               // it is not escaped with two "$$".
               int idx = find (range[1], m_longMsg, "$(");

               if (-1 != idx) {
                  if (0 < idx) {
                     // Check if the marker is escaped. If that is the case, then
                     // ignore this marker and look for another.
                     if (m_longMsg[idx-1] == '$') {

                        // Adding only one so a check for end of string is not
                        // necessary. The next search will start with the '('.
                        range[1] = idx + 1;
                        continue;
                     }
                  }

                  int endidx = find (idx, m_longMsg, ")");
                  if (-1 == endidx) {
                     // No end bracket, this is an error but we ignore it as the
                     // contract states that invalid properties will be ignored.
                     return false;

                  } else {
                     range[0] = idx;
                     range[1] = endidx + 1;
                     return true;
                  }

               } else {
                  // Did not find the "$(" marker.
                  return false;
               }
            }
         }

         //~ debug {
            //~ writefln ("%s", m_longMsg);
         //~ }

         char[] resolvedStr;

         if (findNextProperty ()) {

            if (range[0] > 0) {
               resolvedStr = m_longMsg [0..range[0]].dup;

            } else {
               resolvedStr = "";
            }

            do {
               //~ debug {
                  //~ writefln ("%d %d", range[0], range[1]);
               //~ }

               auto propname = m_longMsg[range[0]..range[1]];

               Box value = getProperty(propname[2..$-1]);

               // If the property could not be found, then append the property
               // name as read from the input string. If the property is found,
               // append the value.
               if (value.type is null) {
                  resolvedStr ~= propname;

               } else {
                  resolvedStr ~= value.toString ();
               }

               //~ debug {
                  //~ writefln ("Property (%s) ==> (%s)", propname, value);
               //~ }

            } while (findNextProperty ());

            // If there is text after the last property, append it to the resolved
            // string.
            if (range[1] < m_longMsg.length) {
               resolvedStr ~= m_longMsg[range[1]..$];
            }

         } else {

            resolvedStr = m_longMsg;
         }

         //~ debug {
            //~ writefln ("RESOLVED: %s", resolvedStr);
         //~ }

         return resolvedStr;
      }
   }


   /**
    * Property to return the long message as supplied without any parsing. If
    * no long message is set, an empty string is returned.
    */
   string unparsedLongMsg () {
      if (m_longMsg is null) {
         return "";

      } else {
         return m_longMsg;
      }
   }


   /* NOTE: If I change char[] to string, the compiler cannot deduce the
    *       argument types for the template.
    */
   typeof(this) setProperty(T) (string property, T value) {
      static if (is (T : Box)) {
         m_propertyBag [property] = value;
      } else {
         m_propertyBag [property] = box (value);
      }
      return this;
   }


   /**
    * Retrieve the value of the named property.
    *
    * The property will be searched through all the bags of the causality chain
    * starting with the head of the chain. If the exception instance is not
    * the head of the chain, the the head of the chain is located as the
    * starting point for the search.
    */
   Box getProperty (string property) {
      /* Find the head of the chain by iterating through the parent cause
       * links.
       */
      if (m_parentCause !is null) {
         Exception2 pc = m_parentCause.get ();
         if (pc !is null) {
            return pc.getProperty (property);
         }
      }

      /* If the instance has no parent clause, then start the search.
       */
      return getPropertyDescending (property);
   }


   /**
    * Search for the named property in this class's property bag and, if not
    * found, continue the search through all the cause exception property bags.
    */
   protected Box getPropertyDescending (string property) {
      if (property in m_propertyBag) {
         return m_propertyBag [property];
      } else {
         if (m_cause !is null) {
            return m_cause.getPropertyDescending (property);
         } else {
            return Box();
         }
      }
   }


   bool hasProperty (string property) {
      /* Find the head of the chain by iterating through the parent cause
       * links.
       */
      if (m_parentCause !is null) {
         Exception2 pc = m_parentCause.get ();
         if (pc !is null) {
            return pc.hasProperty (property);
         }
      }

      /* If the instance has no parent clause, then start the search.
       */
      return hasPropertyDescending (property);
   }


   protected bool hasPropertyDescending (string property) {
      if (property in m_propertyBag) {
         return true;
      } else {
         if (m_cause !is null) {
            return m_cause.hasPropertyDescending (property);
         } else {
            return false;
         }
      }
   }


   Box[string] propertyBag () {
      return m_propertyBag;
   }


   string toString () {

      /* Format properties for output. The properties are listed one to a line
       * of format 'name : value'.
       */
      char[] pstr;

      foreach (p; m_propertyBag.keys.sort) {
         pstr ~= "\n" ~ p ~ ": " ~ m_propertyBag[p].toString ();
      }

      /* Prepend the exception type to the message.
       */
      char[] tmpMsg = this.classinfo.name ~ ": " ~ msg;

      /* If the exception has properties, append the properties to the exception
       * message separated by an empty line.
       */
      if (pstr.length > 0) {
         tmpMsg ~= "\n" ~ pstr;
      }

      /* If the exception has a cause, append the cause descripton separated by
       * an empty line and after the words "caused by: ".
       */
      if (m_cause !is null) {
         tmpMsg ~= "\n\ncaused by: " ~ m_cause.toString ();
      }

      return tmpMsg;
   }


   protected void setParentCause (Exception2 parentCause) {
      m_parentCause = new WeakObjectReference!(Exception2) (parentCause);
   }


   protected Box[string]                      m_propertyBag;

   protected string                           m_longMsg;
   protected Exception2                       m_cause;
   protected WeakObjectReference!(Exception2) m_parentCause;
}

/**
 * Adapt an Exception instance to the Exception2 interface. This is to allow
 * the consumption of Exception instances as the cause of the Exception2.
 */
private class ExceptionAdaptor : Exception2 {

   private this (Exception e) {
      super (e.msg);
      m_wrapped = e;
   }

   override typeof(this) initCause (Exception cause) {
      assert (0, "Cannot call initCause on ExceptionAdaptor.");
   }

   override typeof(this) setLongMsg (string longMsg) {
      assert (0, "Cannot call setLongMsg on ExceptionAdaptor.");
   }

   typeof(this) setProperty(T) (string property, T value) {
      assert (0, "Cannot call setProperty on ExceptionAdaptor.");
   }

   override protected Box getPropertyDescending (string property) {
      return Box();
   }

   override protected bool hasPropertyDescending (string property) {
      return false;
   }

   override string toString () {
      return m_wrapped.toString ();
   }

   Exception m_wrapped;
}

template AssertAsserted (string OP, string msg) {
   const string AssertAsserted =
   `
   {
      bool asserted = false;
      try {`
         ~OP~`;
      } catch (AssertError e) {
         asserted = true;
      }
      assert (asserted, "`~msg~`");
   }
   `;
}

unittest {
   Exception genericException = new Exception ("This is a generic exception");

   Exception badException = new Exception ("Bad Exception");

   {
      ExceptionAdaptor ea = new ExceptionAdaptor (genericException);

      assert (genericException.toString() == ea.toString()
         , "Failed ExceptionAdaptor.toString test. The toString method must "
           "call the wrapped Exception's toString method.");

      assert (ea.getPropertyDescending ("property").type is null
         , "Failed ExceptionAdaptor.getPropertyDescending test. The method "
           "must return an empty box.");

      assert (ea.getProperty ("property").type is null
         , "Failed ExceptionAdaptor.getProperty test. Expecting an empty "
           "box but received '"~ea.getProperty ("property").toString()~"'.");

      assert (false == ea.hasPropertyDescending ("property")
         , "Failed ExceptionAdaptor.hasPropertyDescending test. The method "
           "must return false.");

      assert (false == ea.hasProperty ("property")
         , "Failed ExceptionAdaptor.hasProperty test. Expecting 'false' "
           "but received 'true'.");

      mixin (AssertAsserted!(`ea.initCause (badException)`
         , "Failed ExceptionAdaptor.initCause call test"));

      mixin (AssertAsserted!(`ea.setLongMsg ("Long message")`
         , "Failed ExceptionAdaptor.setLongMsg call test"));

      mixin (AssertAsserted!(`ea.setProperty ("property", 0)`
         , "Failed ExceptionAdaptor.setProperty call test"));
   }

   {
      Exception2 exc = new Exception2 ("An Exception");

      assert ("dbapi.internal.exception2.Exception2: An Exception" == exc.toString ()
         , "Failed Exception2.toString test. Expecting "
           "'dbapi.internal.exception2.Exception2: An Exception' but received "
           "'"~exc.toString ()~"'.");

      assert ("An Exception" == exc.msg
         , "Failed Exception2.msg test. Expecting 'An Exception' but received "
           "'"~exc.msg~"'");

      assert (exc.cause is null
         , "Failed Exception2.cause test. Expecting null but cause was set.");

      assert ("An Exception" == exc.longMsg
         , "Failed Exception2.longMsg test. Expecting 'An Exception' but "
           "received '"~exc.longMsg~"'.");

      assert (0 == exc.unparsedLongMsg.length
         , "Failed Exception2.unparsedLongMsg test. Expecting an empty string "
           "but received '"~exc.unparsedLongMsg~"'.");

      {
         Box p = exc.getProperty ("property");

         assert (p.type is null
            , "Failed Exception2.getProperty test. Expecting an empty "
              "Box but received '"~p.toString()~"'.");
      }

      assert (false == exc.hasProperty ("property")
         , "Failed Exception2.hasProperty test. Expecting 'false' but received "
           "'true'.");

      assert (0 == exc.propertyBag.length
         , "Failed Exception2.propertyBag test. Expecting an empty associative "
           "array but received a populated one.");

      exc.setLongMsg ("This is a long message $(TestProperty)");

      assert ("An Exception" == exc.msg
         , "Failed Exception2.msg test (2). Expecting 'An Exception' but "
           "received '"~exc.msg~"'");

      assert ("This is a long message $(TestProperty)" == exc.longMsg
         , "Failed Exception2.longMsg test (2). Expecting "
           "'This is a long message $(TestProperty)' but received '"
           ~exc.longMsg~"'.");

      assert ("This is a long message $(TestProperty)" == exc.unparsedLongMsg
         , "Failed Exception2.unparsedLongMsg test (2). Expecting "
           "'This is a long message $(TestProperty)' but received '"
           ~exc.unparsedLongMsg~"'.");

      exc.setProperty ("testproperty", "MyTestPropertyFilled");

      assert (1 == exc.propertyBag.length
         , "Failed Exception2.propertyBag test (2). Expecting an associative "
           "array with one element but received one with ("~
           str(exc.propertyBag.length)~") elements.");

      {
         Box p = exc.getProperty ("testproperty");

         assert (p.type !is null
            , "Failed Exception2.getProperty test (2). Expecting a "
              "populated Box but received an empty box.");

         assert ("MyTestPropertyFilled" == p.toString()
            , "Failed Exception2.getProperty test (2). Expecting the "
              "property value to be 'MyTestPropertyFilled' but received '"
              ~p.toString()~"'.");
      }

      assert (true == exc.hasProperty ("testproperty")
         , "Failed Exception2.hasProperty test (2). Expecting 'true' but received "
           "'false'.");

      assert ("This is a long message $(TestProperty)" == exc.longMsg
         , "Failed Exception2.longMsg test (3). Expecting "
           "'This is a long message $(TestProperty)' but received '"
           ~exc.longMsg~"'.");

      exc.setProperty ("TestProperty", "MyTestPropertyFilled");

      assert ("This is a long message MyTestPropertyFilled" == exc.longMsg
         , "Failed Exception2.longMsg test (4). Expecting "
           "'This is a long message MyTestPropertyFilled' but received '"
           ~exc.longMsg~"'.");
   }


   {
      Exception2 exc = new Exception2 ("An Exception");
      exc.initCause (genericException);

      assert (
         "dbapi.internal.exception2.Exception2: An Exception\n\ncaused by: This is a "
         "generic exception" == exc.toString()
         , "Failed ExceptionAdaptor in Exception2.toString() test. Expecting '"
           "dbapi.internal.exception2.Exception2: An Exception\n\ncaused by: This is a "
           "generic exception' but received '"~exc.toString()~"'");

      assert ("An Exception" == exc.msg
         , "Failed ExceptionAdaptor in Exception2.msg test. Expecting "
           "'An Exception' but received '"~exc.msg~"'");

      assert (exc.cause !is null
         , "Failed ExceptionAdaptor in Exception2.cause test. Expecting cause "
           "but received null.");

      assert ("An Exception" == exc.longMsg
         , "Failed ExceptionAdaptor in Exception2.longMsg test. Expecting "
           "'An Exception' but received '"~exc.longMsg~"'.");

      assert (0 == exc.unparsedLongMsg.length
         , "Failed ExceptionAdaptor in Exception2.unparsedLongMsg test. "
           "Expecting an empty string but received '"~exc.unparsedLongMsg~"'.");

      {
         Box p = exc.getProperty ("property");

         assert (p.type is null
            , "Failed ExceptionAdaptor in Exception2.getProperty test. "
              "Expecting an empty Box but received '"~p.toString()~"'.");
      }

      assert (false == exc.hasProperty ("property")
         , "Failed ExceptionAdaptor in Exception2.hasProperty test. Expecting "
           "'false' but received 'true'.");

      assert (0 == exc.propertyBag.length
         , "Failed ExceptionAdaptor in Exception2.propertyBag test. Expecting "
           "an empty associative array but received a populated one.");

      exc.setLongMsg ("This is a long message $(TestProperty)");

      assert ("An Exception" == exc.msg
         , "Failed Exception2.msg test (2). Expecting 'An Exception' but "
           "received '"~exc.msg~"'");

      assert ("This is a long message $(TestProperty)" == exc.longMsg
         , "Failed ExceptionAdaptor in Exception2.longMsg test (2). Expecting "
           "'This is a long message $(TestProperty)' but received '"
           ~exc.longMsg~"'.");

      assert ("This is a long message $(TestProperty)" == exc.unparsedLongMsg
         , "Failed ExceptionAdaptor in Exception2.unparsedLongMsg test (2). Expecting "
           "'This is a long message $(TestProperty)' but received '"
           ~exc.unparsedLongMsg~"'.");

      exc.setProperty ("testproperty", "MytestpropertyFilled");

      assert (1 == exc.propertyBag.length
         , "Failed ExceptionAdaptor in Exception2.propertyBag test (2). Expecting an associative "
           "array with one element but received one with ("~
           str(exc.propertyBag.length)~") elements.");

      {
         Box p = exc.getProperty ("testproperty");

         assert (p.type !is null
            , "Failed ExceptionAdaptor in Exception2.getProperty test (2). Expecting a "
              "populated Box but received an empty box.");

         assert ("MytestpropertyFilled" == p.toString()
            , "Failed ExceptionAdaptor in Exception2.getProperty test (2). Expecting the "
              "property value to be 'MytestpropertyFilled' but received '"
              ~p.toString()~"'.");
      }

      assert (true == exc.hasProperty ("testproperty")
         , "Failed ExceptionAdaptor in Exception2.hasProperty test (2). Expecting 'true' but received "
           "'false'.");

      assert ("This is a long message $(TestProperty)" == exc.longMsg
         , "Failed ExceptionAdaptor in Exception2.longMsg test (3). Expecting "
           "'This is a long message $(TestProperty)' but received '"
           ~exc.longMsg~"'.");

      exc.setProperty ("TestProperty", "MyTestPropertyFilled");

      assert ("This is a long message MyTestPropertyFilled" == exc.longMsg
         , "Failed ExceptionAdaptor in Exception2.longMsg test (4). Expecting "
           "'This is a long message MyTestPropertyFilled' but received '"
           ~exc.longMsg~"'.");

      Exception2 cause = exc.cause;
      
      assert ("This is a generic exception" == cause.msg);
      assert ("This is a generic exception" == cause.longMsg);
      assert (cause.cause is null);
      assert (cause.getProperty ("noproperty").type is null);
      assert (cause.getProperty ("testproperty").type !is null);
      assert ("MytestpropertyFilled" == cause.getProperty ("testproperty").toString());
      assert (cause.getProperty ("TestProperty").type !is null);
      assert ("MyTestPropertyFilled" == cause.getProperty ("TestProperty").toString());
      assert (genericException.toString() == cause.toString()
         , "Failed ExceptionAdaptor.toString test. The toString method must "
           "call the wrapped Exception's toString method.");
   }
   
   {
      Exception2 exc = new Exception2 ("An Exception2");
      exc.initCause (new Exception2 ("A cause Exception2"));
   }

}
